﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Spanner
{
    public class Context
    {
        /// <summary>
        /// The default location where Knockout is taken from.
        /// </summary>
        public const string DefaultKnockoutUrl = @"http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js";

        /// <summary>
        /// Set this to change where Knockout is loaded instead of from DefaultKnockoutUrl.
        /// </summary>
        public string KnockoutUrl;
        
        // Used to control indentation levels in the output.
        internal int Indentation = 0;

        // The directory where output files will be written.
        internal string OutputPath;

        // Subdirectories of OutputPath for HTML, JavaScript, CSS, and media files.
        internal string HtmlPath;
        internal string CssPath;
        internal string JSPath;
        internal string MediaPath;

        // If true, all output is to a single file per view;
        // otherwise view models are written to separate files.
        internal bool SingleFile;

        // Dependencies of the current view.
        internal HashSet<Model> PendingDependencies = new HashSet<Model>();

        // The set of dependencies that have been written out through this context.
        // This field is not used when in single-file mode.
        internal HashSet<Model> WrittenDependencies = new HashSet<Model> { };

        // This is the current output target.
        internal TextWriter TW;

        // We always include Knockout.
        internal LibraryModel Knockout;

        // Every view model implicitly depends on the Spanner runtime.
        internal LibraryModel SpannerRuntime;

        public Context(string outputPath, bool singleFile = true)
        {
            OutputPath = outputPath;
            HtmlPath = Path.Combine(OutputPath, "html");
            CssPath = Path.Combine(OutputPath, "css");
            JSPath = Path.Combine(OutputPath, "js");
            MediaPath = Path.Combine(OutputPath, "media");
            SingleFile = singleFile;

            Directory.CreateDirectory(HtmlPath);
            Directory.CreateDirectory(CssPath);
            Directory.CreateDirectory(JSPath);
            Directory.CreateDirectory(MediaPath);

            Knockout = new ExternalNonSpannerLibraryModel("ko", KnockoutUrl ?? DefaultKnockoutUrl);

            SpannerRuntime = new NonSpannerLibraryModel("spanner", Spanner.Properties.Resources.SpannerRuntime, Knockout);
        }

        internal void WriteHtml(Html html)
        {
            html.WriteHtml(this);
        }

        /// <summary>
        /// Write out the given web page and code for all its dependencies.
        /// </summary>
        /// <param name="webPage"></param>
        public void WriteWebPage(WebPage webPage)
        {
            // Create an output file and write out the view HTML.
            // The view's JavascriptRefs and JavascriptCode elements
            // should handle writing out the dependencies
            // (to other files if we are not in single-file mode).
            using (var tw = new StreamWriter(Path.Combine(HtmlPath, webPage.Name + ".html")))
            {
                CurrModelContext = new ModelContext { Model = webPage };
                RegisterAllImportedLibraryModelVars();
                TW = tw;
                // XXX NEED TO ADD DEPENDENCIES HERE SINCE THEY MAY APPEAR IN ANY ORDER IN THE HTML BINDINGS.
                webPage.Html.WriteHtml(this); // This should include something to write the view model JavaScript.
                TW = null;
                CurrModelContext = null;
            }
        }

        internal void WriteLine()
        {
            TW.WriteLine();
        }

        internal void WriteIndentation() {
            for (var i = 0; i < Indentation; i++) TW.Write("  ");
        }

        internal void WriteIndentedLine(string fmt, params object[] args)
        {
            WriteIndentation();
            TW.WriteLine(fmt, args);
        }

        internal void WriteIndented(string fmt, params object[] args)
        {
            WriteIndentation();
            TW.Write(fmt, args);
        }

        internal void WriteLine(string fmt, params object[] args)
        {
            TW.WriteLine(fmt, args);
        }

        internal void Write(string fmt, params object[] args)
        {
            TW.Write(fmt, args);
        }

        internal void WritePlain(string text)
        {
            TW.Write(text);
        }

        internal void WritePlainLine(string text)
        {
            TW.WriteLine(text);
        }

        internal void WriteTerm(ITerm term, JSorHtml what)
        {
            if (term == null)
            {
                WritePlain("null");
                return;
            }
            term.Write(this, what);
        }

        internal ModelContext CurrModelContext = null;

        internal void WriteViewModelJSScript()
        {
            // We need the ordered list of model dependencies.
            var visited = new HashSet<Model> { };
            var orderedDependencies = new List<Model> { Knockout, SpannerRuntime }; // These are always present.
            TopSortDependencies(CurrModelContext.Model.ModelDependencies, visited, orderedDependencies);

            // We need to include the HTML for template models in-line (I can't see much value in
            // writing them to separate files).
            WriteTemplateModelsHtml(orderedDependencies);

            // Write out references to external libraries.
            WriteExternalDependencies(orderedDependencies);

            if (SingleFile)
            {   
                WriteIndentedLine("<script>");
                Indentation++;
                WriteModelDependenciesCodeInThisFile(orderedDependencies);
                WriteModelJS();
                WriteViewModelBinder();
                Indentation--;
                WriteIndentedLine("</script>");
            }
            else
            {
                WriteModelDependencyReferences(orderedDependencies);
                WriteIndentedLine("<script>");
                Indentation++;
                WriteModelJS();
                WriteViewModelBinder();
                Indentation--;
                WriteIndentedLine("</script>");
                WriteModelDependenciesCodeInSeparateFiles(orderedDependencies);
            }
        }

        // We need to set up model contexts for templates at the point where we write out their HTML.
        internal Dictionary<Model, ModelContext> TemplateModelContexts = new Dictionary<Model, ModelContext> { };

        internal void WriteTemplateModelsHtml(IEnumerable<Model> orderedDependencies)
        {
            var savedCurrModelContext = CurrModelContext;

            foreach (var templateModel in orderedDependencies.Where(x => x is TemplateModel).Cast<TemplateModel>())
            {
                WriteIndentedLine("<script id='{0}' type='text/html'>", templateModel.TemplateID);
                Indentation++;
                var templateModelContext = new ModelContext { Model = templateModel };
                TemplateModelContexts[templateModel] = templateModelContext;
                CurrModelContext = templateModelContext;
                WriteHtml(templateModel.Html);
                Indentation--;
                WriteIndentedLine("</script>");
            }

            CurrModelContext = savedCurrModelContext;
        }

        internal void WriteExternalDependencies(IEnumerable<Model> orderedDependencies)
        {
            foreach (var nonSpannerModel in orderedDependencies.Where(x => x.NonSpannerJSUrl != null))
            {
                WriteHtml(UI.Script().WithAttr("src", nonSpannerModel.NonSpannerJSUrl));
            }
        }

        // Take the transitive closure of a list of model dependencies and return their
        // topologically sorted order.  The order of members in a cycle is undefined.
        internal void TopSortDependencies(
            Model[] dependencies,
            HashSet<Model> visited,
            List<Model> order
        ) {
            if (dependencies == null) return;
            foreach (var d in dependencies)
            {
                if (visited.Contains(d)) continue;
                visited.Add(d);
                TopSortDependencies(d.ModelDependencies, visited, order);
                order.Add(d);
            }
        }

        internal void WriteModelDependenciesCodeInThisFile(List<Model> orderedDependencies)
        {
            var savedCurrModelContext = CurrModelContext;

            foreach (var dependency in orderedDependencies.Where(x => x.NonSpannerJSUrl == null))
            {
                CurrModelContext = ( dependency is TemplateModel
                                   ? TemplateModelContexts[dependency]
                                   : new ModelContext { Model = dependency }
                                   );
                WriteModelJS();
            }

            CurrModelContext = savedCurrModelContext;
        }

        internal void WriteModelDependencyReferences(List<Model> orderedDependencies)
        {
            foreach (var dependency in orderedDependencies.Where(x => x.NonSpannerJSUrl == null))
            {
                var scriptPath = string.Format("../js/{0}.js", dependency.Name);
                WriteHtml(UI.Script().WithAttr("src", scriptPath));
            }
        }

        internal void WriteModelDependenciesCodeInSeparateFiles(List<Model> orderedDependencies)
        {
            var savedCurrModelContext = CurrModelContext;
            var savedTW = TW;

            foreach (var dependency in orderedDependencies.Where(x => x.NonSpannerJSUrl == null && !WrittenDependencies.Contains(x)))
                using (var tw = new StreamWriter(Path.Combine(JSPath, dependency.Name + ".js")))
                {
                    CurrModelContext = ( dependency is TemplateModel
                                       ? TemplateModelContexts[dependency]
                                       : new ModelContext { Model = dependency }
                                       );
                    TW = tw;
                    WriteModelJS();
                    WrittenDependencies.Add(dependency); // Only write each model once.
                }

            TW = savedTW;
            CurrModelContext = savedCurrModelContext;
        }

        internal void WriteModelJS()
        {
            // If this is a non-spanner model then it comes with its own implementation.

            var nonSpannerImplementationJS = CurrModelContext.Model.NonSpannerImplementationJS;

            if (nonSpannerImplementationJS != null)
            {
                WritePlain(nonSpannerImplementationJS);
                return;
            }

            var viewName = CurrModelContext.Model.Name;

            var initParam = CurrModelContext.Model.InitParam;

            var initParamName = (initParam == null ? "" : VarName(initParam));

            var initAct = CurrModelContext.Model.InitAct;

            var isLibraryModel = CurrModelContext.Model.IsLibraryModel;

            var isTemplateModel = CurrModelContext.Model.IsTemplateModel;

            if (isLibraryModel) AddVarDependenciesForCurrLibraryModel();

            if (isTemplateModel) AddVarDependenciesForCurrTemplateModel();

            // If this is a global model, we need to collect the IVar fields here.

            var prefix = (isLibraryModel ? "var " + viewName + " = new (" : "");

            var suffix = (isLibraryModel ? ")()" : "");

            // We need to know the dependencies for the initialisation action (if any).
            if (initAct != null) foreach (var x in ((ITerm)initAct).Dependencies()) AddVarDependency(x);

            // View models don't take parameters, but templates do.
            WriteIndentedLine("{0}function {1}({2}) {{", prefix, viewName, initParamName);
            Indentation++;

            WriteIndentedLine("var self = this;");

            foreach (var x in CurrModelContext.ObsVars)
            {
                if (x.Kind == VarKind.Param || x.Kind == VarKind.ObsVarField) continue; // These aren't part of the view model.

                WriteIndented("self.{0} = ", VarName(x));
                x.WriteInitialValue(this);
                WriteLine(";");
            }

            if (initAct != null)
            {
                WriteIndentation();
                WriteTerm((ITerm)UI.Do(initAct), JSorHtml.JS);
                WriteLine();
            }

            Indentation--;
            WriteIndentedLine("}}{0};", suffix);

        }

        internal void WriteViewModelBinder()
        {
            WriteIndentedLine("spanner.windowOnLoad(function () {{ ko.applyBindings(new {0}()); }});", CurrModelContext.Model.Name);
        }

        internal Dictionary<IVar, string> ObsVarToModelName = new Dictionary<IVar, string> { };

        internal void AddVarDependency(IVar x)
        {
            if (CurrModelContext.IsImportedIVar(x)) return; // Don't double-count vars.

            var vars = CurrModelContext.ObsVars;

            // Nothing to do if we've already seen this variable.

            if (vars.Contains(x)) return; // Yes, this whole algorithm is O(n^2)...

            // There's a problem if this var has been used in another view!

            var registeredVarModelName = (string)null;
            if (ObsVarToModelName.TryGetValue(x, out registeredVarModelName))
            {
                var varTypeIsFn = x.GetType()
                                   .GetGenericArguments()[0]
                                   .GetInterfaces()
                                   .Contains(typeof(IFn));

                // Functions can be mutually recursive.
                if (varTypeIsFn && registeredVarModelName == CurrModelContext.Model.Name)
                    return;

                throw new ApplicationException("Variables cannot be shared between models: " +
                    VarName(x) + " appears in both " + CurrModelContext.Model.Name +
                    " and " + registeredVarModelName);
            }

            // We haven't seen this var anywhere before.  Let's record its owner view.

            ObsVarToModelName[x] = CurrModelContext.Model.Name;

            // Add this var's dependencies from its initial value, then add the var itself.

            foreach(var y in x.Dependencies()) AddVarDependency(y);

            vars.Add(x);
        }

        internal string VarName(IVar x)
        {
            return CurrModelContext.VarName(x);
        }

        internal void PushForeachObsVar(IVar x)
        {
            CurrModelContext.ForEachObsVars.Add(x);
        }

        internal void PopForeachObsVar()
        {
            CurrModelContext.ForEachObsVars.RemoveAt(CurrModelContext.ForEachObsVars.Count - 1);
        }

        internal void WriteIVar(JSorHtml what, IVar x, bool deref = true)
        {
            if (CurrModelContext == null)
            {
                throw new ApplicationException("You cannot refer to variables outside models: " + x.BaseName);
            }

            // Function parameters are never qualified or implicitly dereferenced.
            if (x.Kind == VarKind.Param && !CurrModelContext.ForEachObsVars.Contains(x))
            {
                Write(VarName(x));
                return;
            }

            // ObsVarFields are present only to make working with objects with ObsVar
            // fields more convenient.
            if (x.Kind == VarKind.ObsVarField)
            {
                ((IObsVarField)x).Write(this, what);
                if (deref) Write("()");
                return;
            }

            var importedName = (string)null;
            if (CurrModelContext.ImportedLibraryModelIVarToName.TryGetValue(x, out importedName))
            {
                Write("{0}", importedName);
                return;
            }

            if (what == JSorHtml.JS)
            {
                Write("{0}{1}{2}", (x.Kind == VarKind.Param ? "" : "self."), VarName(x), (deref ? "()" : ""));
                return;
            }

            // Okay, we must have what == JSorHtml.Html
        
            // Ensure we have this variable and its dependencies in the view model.

            AddVarDependency(x);

            // If we are not in a 'foreach' context, then we just emit the variable namew
            // optionally dereferencing it).

            var foreachVars = CurrModelContext.ForEachObsVars;

            var n = foreachVars.Count;

            if (n == 0)
            {
                Write(VarName(x) + (deref ? "()" : ""));
                return;
            }

            // If we are in a 'foreach' context, we do something different depending on whether this is
            // a 'foreach' var or not.

            var i = (n - 1) - foreachVars.IndexOf(x); // i = 0 is innermost, i = n is "not in the list".

            // This isn't a foreach var.  We need Knockout's '$root' qualifier.

            if (i == n)
            {
                Write("$root." + VarName(x) + (deref ? "()" : ""));
                return;
            }

            // This is a foreach var.  Is it the topmost or not?

            Write((i == 0) ? "$data" : "$parents[" + (i - 1) + "]");
        }

        // We need to include the variable holding the ID of the HTML template.
        internal void AddVarDependenciesForCurrTemplateModel()
        {
            var templateModel = (TemplateModel)CurrModelContext.Model;
            AddVarDependency(templateModel.TemplateIDVar);
        }

        internal void AddVarDependenciesForCurrLibraryModel()
        {
            var model = CurrModelContext.Model;
            var modelName = model.Name;
            var varFields = ModelVarFields(model);
            foreach (var varField in varFields)
            {
                var n = modelName + "." + varField.Name;
                var v = (IVar)(varField.GetValue(model));
                if (v == null) throw new ApplicationException("Model field " + n + " cannot be null.");
                AddVarDependency(v);
            }
        }

        internal static IEnumerable<FieldInfo> ModelVarFields(Model model)
        {
            var modelType = model.GetType();
            var varType = typeof(IVar);
            var varFields = modelType.GetFields().Where(x => varType.IsAssignableFrom(x.FieldType));
            return varFields;
        }

        internal void RegisterAllImportedLibraryModelVars()
        {
            var dependencies = CurrModelContext.Model.ModelDependencies;

            if (dependencies == null) return;

            foreach (var model in dependencies) RegisterImportedLibraryModelVars(model);
        }

        internal void RegisterImportedLibraryModelVars(Model model)
        {
            var modelName = model.Name;
            foreach (var varField in ModelVarFields(model))
            {
                var fn = varField.Name;
                var n = modelName + "." + fn;
                var v = (IVar)(varField.GetValue(model));
                v.BaseName = fn;
                if (v == null) throw new ApplicationException("Model field " + n + " cannot be null.");
                CurrModelContext.ImportedLibraryModelIVarToName[v] = n;
            }
        }
    }
}
